var CryptoJS = require('../crypto/index');

;(function () {
  'use strict';

  // Check the existence of module and module.exports to detect node
  var isNode = (typeof module != 'undefined' && typeof module.exports != 'undefined');

  function OAuthSignature() {
  }

  OAuthSignature.prototype.generate = function (httpMethod, url, parameters, consumerSecret, tokenSecret, options) {
    var signatureBaseString = new SignatureBaseString(httpMethod, url, parameters).generate();
    var encodeSignature = true;
    if (options) {
      encodeSignature = options.encodeSignature;
    }
    return new HmacSha1Signature(signatureBaseString, consumerSecret, tokenSecret).generate(encodeSignature);
  };

  // Specification: http://oauth.net/core/1.0/#anchor14
  // url: if the scheme is missing, http will be added automatically
  function SignatureBaseString(httpMethod, url, parameters) {
    parameters = new ParametersLoader(parameters).get();
    this._httpMethod = new HttpMethodElement(httpMethod).get();
    this._url = new UrlElement(url).get();
    this._parameters = new ParametersElement(parameters).get();
    this._rfc3986 = new Rfc3986();
  }

  SignatureBaseString.prototype = {
    generate: function () {
      // HTTP_METHOD & url & parameters
      return this._rfc3986.encode(this._httpMethod) + '&'
        + this._rfc3986.encode(this._url) + '&'
        + this._rfc3986.encode(this._parameters);
    }
  };

  function HttpMethodElement(httpMethod) {
    this._httpMethod = httpMethod || '';
  }

  HttpMethodElement.prototype = {
    get: function () {
      return this._httpMethod.toUpperCase();
    }
  };

  function UrlElement(url) {
    this._url = url || '';
  }

  UrlElement.prototype = {
    get: function () {
      // The following is to prevent js-url from loading the window.location
      if (!this._url) {
        return this._url;
      }

      // FIXME: Make this behaviour explicit by returning warnings
      if (this._url.indexOf('://') == -1) {
        this._url = 'http://' + this._url;
      }

      // Handle parsing the url in node or in browser
      var parsedUrl = this.parseInNode(),
        // FIXME: Make this behaviour explicit by returning warnings
        scheme = (parsedUrl.scheme || 'http').toLowerCase(),
        // FIXME: Make this behaviour explicit by returning warnings
        authority = (parsedUrl.authority || '').toLocaleLowerCase(),
        path = parsedUrl.path || '',
        port = parsedUrl.port || '';

      // FIXME: Make this behaviour explicit by returning warnings
      if ((port == 80 && scheme == 'http')
        || (port == 443 && scheme == 'https')) {
        port = '';
      }
      var baseUrl = scheme + '://' + authority;
      baseUrl = baseUrl + (!!port ? ':' + port : '');
      // FIXME: Make this behaviour explicit by returning warnings
      if (path == '/' && this._url.indexOf(baseUrl + path) === -1) {
        path = '';
      }
      this._url =
        (scheme ? scheme + '://' : '')
        + authority
        + (port ? ':' + port : '')
        + path;
      return this._url;
    },
    parseInNode: function () {
      var url = require('./url'),
        parsedUri = url.parse(this._url),
        scheme = parsedUri.scheme;
      // strip the ':' at the end of the scheme added by the url module
      if (scheme.charAt(scheme.length - 1) == ":") {
        scheme = scheme.substring(0, scheme.length - 1);
      }
      return {
        scheme: scheme,
        authority: parsedUri.host,
        port: parsedUri.port,
        path: parsedUri.path
      };
    }
  };

  function ParametersElement(parameters) {
    // Parameters format: { 'key': ['value 1', 'value 2'] };
    this._parameters = parameters || {};
    this._sortedKeys = [];
    this._normalizedParameters = [];
    this._rfc3986 = new Rfc3986();
    this._sortParameters();
    this._concatenateParameters();
  }

  ParametersElement.prototype = {
    _sortParameters: function () {
      var key,
        encodedKey;
      for (key in this._parameters) {
        if (this._parameters.hasOwnProperty(key)) {
          encodedKey = this._rfc3986.encode(key);
          this._sortedKeys.push(encodedKey);
        }
      }
      this._sortedKeys.sort();
    },
    _concatenateParameters: function () {
      var i;
      for (i = 0; i < this._sortedKeys.length; i++) {
        this._normalizeParameter(this._sortedKeys[i]);
      }
    },
    _normalizeParameter: function (encodedKey) {
      var i,
        key = this._rfc3986.decode(encodedKey),
        values = this._parameters[key],
        encodedValue;
      values.sort();
      for (i = 0; i < values.length; i++) {
        encodedValue = this._rfc3986.encode(values[i]);
        this._normalizedParameters.push(encodedKey + '=' + encodedValue)
      }
    },
    get: function () {
      return this._normalizedParameters.join('&');
    }
  };

  function ParametersLoader(parameters) {
    // Format: { 'key': ['value 1', 'value 2'] }
    this._parameters = {};
    this._loadParameters(parameters || {});
  }

  ParametersLoader.prototype = {
    _loadParameters: function (parameters) {
      if (parameters instanceof Array) {
        this._loadParametersFromArray(parameters);
      } else if (typeof parameters === 'object') {
        this._loadParametersFromObject(parameters);
      }
    },
    _loadParametersFromArray: function (parameters) {
      var i;
      for (i = 0; i < parameters.length; i++) {
        this._loadParametersFromObject(parameters[i]);
      }
    },
    _loadParametersFromObject: function (parameters) {
      var key;
      for (key in parameters) {
        if (parameters.hasOwnProperty(key)) {
          this._loadParameterValue(key, parameters[key] || '');
        }
      }
    },
    _loadParameterValue: function (key, value) {
      var i;
      if (value instanceof Array) {
        for (i = 0; i < value.length; i++) {
          this._addParameter(key, value[i]);
        }
        if (value.length == 0) {
          this._addParameter(key, '');
        }
      } else {
        this._addParameter(key, value);
      }
    },
    _addParameter: function (key, value) {
      if (!this._parameters[key]) {
        this._parameters[key] = [];
      }
      this._parameters[key].push(value);
    },
    get: function () {
      return this._parameters;
    }
  };

  function Rfc3986() {

  }

  Rfc3986.prototype = {
    encode: function (decoded) {
      if (!decoded) {
        return '';
      }
      // using implementation from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent?redirectlocale=en-US&redirectslug=JavaScript%2FReference%2FGlobal_Objects%2FencodeURIComponent
      return encodeURIComponent(decoded)
        .replace(/[!'()]/g, escape)
        .replace(/\*/g, "%2A");
    },
    decode: function (encoded) {
      if (!encoded) {
        return '';
      }
      return decodeURIComponent(encoded);
    }
  };

  function HmacSha1Signature(signatureBaseString, consumerSecret, tokenSecret) {
    this._rfc3986 = new Rfc3986();
    this._text = signatureBaseString;
    this._key = this._rfc3986.encode(consumerSecret) + '&' + this._rfc3986.encode(tokenSecret);
    this._base64EncodedHash = new HmacSha1(this._text, this._key).getBase64EncodedHash();
  }

  HmacSha1Signature.prototype = {
    generate: function (encode) {
      return encode === false ?
        this._base64EncodedHash :
        this._rfc3986.encode(this._base64EncodedHash);
    }
  };

  function HmacSha1(text, key) {
    // load CryptoJs in the browser or in node
    this._cryptoJS = CryptoJS;
    this._text = text || '';
    this._key = key || '';
    this._hash = this._cryptoJS.HmacSHA1(this._text, this._key);
  }

  HmacSha1.prototype = {
    getBase64EncodedHash: function () {
      return this._hash.toString(this._cryptoJS.enc.Base64);
    }
  };

  var oauthSignature = new OAuthSignature();
  oauthSignature.SignatureBaseString = SignatureBaseString;
  oauthSignature.HttpMethodElement = HttpMethodElement;
  oauthSignature.UrlElement = UrlElement;
  oauthSignature.ParametersElement = ParametersElement;
  oauthSignature.ParametersLoader = ParametersLoader;
  oauthSignature.Rfc3986 = Rfc3986;
  oauthSignature.HmacSha1Signature = HmacSha1Signature;
  oauthSignature.HmacSha1 = HmacSha1;

  // support for the browser and nodejs
  if (isNode) {
    module.exports = oauthSignature;
  } else {
    window.oauthSignature = oauthSignature;
  }
})();
